• 중요한 것은 개별 객체가 아니라 객체들 사이에 이뤄지는 협력입니다.

책임

  • 객체지향 개발에서 가장 중요힌 능력은 능숙하게 소프트웨어 객체에 할당하는 것입니다.

책임의 분류

  • 객체의 책임은 객체가 무엇을 알고 있는가(knowing)와 무엇을 할 수 있는가(doing)로 구성됩니다.

하는 것(doing)

  • 객체를 생성하거나 계산하는 등의 스스로 하는 것
  • 다른 객체의 행동을 시작시키는 것
  • 다른 객체의 활동을 제어하고 조절하는 것

아는 것(knowing)

  • 개인적인 정보에 관해 아는 것
  • 관련된 객체에 관해 아는 것
  • 자신이 유도하거나 계산할 수 있는 것에 관해 아는 것

공용 인터페이스(public interface)

  • 책임은 객체의 공용 인터페이스를 구성합니다.

책임과 메시지

  • 객체지향 설꼐는 협력에 참여하기 위해 어떤 객체가 어떤 책임을 수행해야 하고 어떤 객체로부터 메시지를 수신할 것인지를 결정하는 것으로부터 시작됩니다.
    • 어떤 클래스가 필요하고 어떤 메서드를 포함해야 하는지를 결정하는 것은 책임과 메시지에 대한 대략적인 윤관을 잡은 후에 시작해도 늦지 않습니다.
  • 메시지가 책임을 의미합니다.

역할

  • 역할은 객칮향 설계의 단순성, 유연성, 재사용성을 뒷받침하는 핵심 개념입니다.
  • 역할은 협력 내에서 다른 객체로 대체할 수 있음을 나타내는 일종의 표식입니다. 협력 안에서 역할은 ‘이 자리는 해당 역할을 수행할 수 있는 어떤 객체라도 대신할 수 있습니다’라고 말하는 것과 같습니다.
  • 동일한 역할을 수행할 수 있다는 것은 해당 객체들이 협력 내에서 동일한 책임의 집합을 수행할 수 있다는 것을 의미합니다.
  • 역할의 개념을 사용하면 유사한 협력을 추상화해서 인지 과부화를 줄일 수 있습니다.
  • 다양한 객체들이 협력에 참여할 수 있기 때문에 협력이 좀 더 유연해지며 다양한 객체들이 동일한 협력에 참여할수 있기 때문에 재사용성이 높아집니다.
  • 협력의 추상화, 대체 가능성

객체의 모양을 결정하는 협력

  • 객체지향 입문자들이 데이터나 클래스를 중심으로 애플리케이션을 설계하는 이유는 협력이라는 문맥을 고려하지 않고 각 객체를 독립적으로 바라보기 때문입니다.
  • 올바른 객체를 설계하기 위해서는 먼저 견고하고 깔끔한 협력을 설계해야 합니다. 협력을 설계한다는 것은 설계에 참여하는 객체들이 주고받을 요청과 응답의 흐름을 결정한다는 것을 의미합니다. 이렇게 결정된 요청과 응답의 흐름은 객체가 협력에 참여하기 위해 수행될 책임이 됩니다.

객체지향 설계 기법

책임-주도 설계(Responsibility-Driven Design)

  • 협력에 필요한 책임들을 식별하고 적합한 객체에게 책임을 할당하는 방식으로 애플리케이션을 설계합니다.
  • 책임-주도 설계 방법은 객체지향 패러다임의 전문가들이 애플리케이션을 개발할 때 어떤 방식으로 사고하고 무엇을 기반으로 의사결정을 내리는지 잘 보여줍니다.
  • 개별적인 객체의 상태가 아니라 객체의 책임과 상호작용에 집중합니다. 결과적으로 시스템은 스스로 자신을 책임질 수 있을 정도로 충분히 자율적인 동시에 다른 객체와 우호적으로 협력할 수 있을 정도록 충분히 협조적인 객체들로 이뤄진 생태계를 구성하게 됩니다.

협조적이고 성실한 객체 시민들로 구성된 객체지향 시스템을 설계하는 절차

  • 시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악합니다.
  • 시스템 책임을 더 작은 책임으로 분할합니다.
  • 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당합니다.
  • 객체가 책임을 수행하는 중에 다른 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾습니다.
  • 해당 객체 또는 역할에게 책임을 할당함으로써 두 객체가 협력하게 합니다.

디자인 패턴

  • 전문가들이 반복적으로 사용하는 해결 방법을 정의해 놓은 설계 템플릿 모음입니다.
  • 디자인 패턴은 공통으로 사용할 수 있는 역할, 책임, 협력의 템플릿입니다. 만약 특정한 상황에 적용 가능한 디자인 패턴을 잘 알고 있다면 책임-주도 설계의 절차를 순차적으로 따르지 않고도 시스템 안에 구현할 역할과 책임, 협력 관계를 빠르고 손쉽게 포착할 수 있을 것입니다.
  • 디자인 패턴은 책임-주도 설계의 결과물인 동시에 지름길입니다.

테스트-주도 개발(Test-Driven Development)

  • 테스트를 먼저 작성하고 테스트를 통과하는 구체적인 코드를 추가하면서 애플리케이션을 완성해가는 방식을 따릅니다.
  • 테스트는 단지 테스트-주도 개발을 통해 얻을 수 있는 별도의 보너스 같은 것이며, 실제 목적은 구체적인 코드를 작성해나가면서 역할, 책임, 협력을 식별하고 식별된 역할, 책임, 협력이 적합한지를 피드백받는 것입니다.
  • 테스트-주도 개발은 책임-주도 설계를 통해 도달해야 하는 목적지를 테스트라는 안전장치를 통해 좀 더 빠르고 견고한 방법으로 도달할 수 있도록 해주는 최상의 설계 프랙티스입니다.
    • 테스트-주도 개발은 책임-주도 설계의 기본 개념과 다양한 원칙과 프랙티스, 패턴을 종합적으로 이해하고 좋은 설계에 대한 감각과 경험을 길러야만 적용할 수 있는 설계 기법입니다.

코드

interface Witness {
  testify: () => string | void;
}

interface TrialHelper {
  setWitness: (witness: Witness) => void;
  callWitness: () => Witness;
}

abstract class Judge {
  health = 0;

  public isOk() {
    return this.health < 2;
  }

  listenToTestimony(trialHelper?: TrialHelper) {
    if (this.isOk()) {
      throw '더 이상 재판을 할 수 없습니다';
    }
    this.health += 1;
    if (!trialHelper) {
      console.log('신경쓸 것 없다');
    } else {
      const message = trialHelper.callWitness().testify();

      return this._listenToTestimony(message || '');
    }
  }
  abstract _listenToTestimony(message: string);
}

class King extends Judge {
  _listenToTestimony(message: string) {
    console.log('King이 받은 증언: ', message);
  }
}

class Queen extends Judge {
  _listenToTestimony(message: string) {
    console.log('Queen이 받은 증언: ', message);
  }
}

class HatDealer implements Witness {
  testify() {
    return '모자딜러가 증언합니다.';
  }
}

class Cook implements Witness {
  testify() {
    throw new Error('도망감');
  }
}

class Alice implements Witness {
  testify() {
    return '앨리스가 증언합니다.';
  }
}

class WhiteRabbit implements TrialHelper {
  private witness: Witness;

  setWitness(witness: Witness) {
    this.witness = witness;
  }

  callWitness() {
    return this.witness;
  }
}

class Trial {
  constructor(private judge: Judge, private trialHelper: TrialHelper, witness?: Witness) {
    if (witness) {
      this.trialHelper.setWitness(witness);
    }
  }

  start() {
    this.judge.listenToTestimony(this.trialHelper);
  }

  changeWitness(witness: Witness) {
    this.trialHelper.setWitness(witness);
  }

  changeJudges(judges: Judge) {
    this.judge = judges;
  }
}

class God {
  static main() {
    const witnessList: Witness[] = [new HatDealer(), new Cook(), new Alice()];
    const trialHelper: TrialHelper = new WhiteRabbit();
    const mainJudge: Judge = new King();

    const trial = new Trial(mainJudge, trialHelper, witnessList[0]);
    trial.start();

    trial.changeWitness(witnessList[1]);
    trial.start();

    const subJudge: Judge = new Queen();
    trial.changeJudges(subJudge);
    trial.changeWitness(witnessList[2]);
    trial.start();
  }

  static main2() {
    const witnessList: Witness[] = [new HatDealer(), new Cook(), new Alice()];
    const trialHelper: TrialHelper = new WhiteRabbit();
    const mainJudge: Judge = new King();

    const trial = new Trial(mainJudge, trialHelper);
    const subJudge: Judge = new Queen();

    witnessList.forEach((w) => {
      try {
        trial.changeWitness(w);
        trial.start();
      } catch (e) {
        trial.changeJudges(subJudge);
        trial.start();
      }
    });
  }
}

// God.main2();
God.main();

참고